/**
 * $version:  0.1 
 * $Date: 2012-03-19 
 *
 * Copyright (C) 2010-2011 Jawa Software. All rights reserved.
 *
 */
package org.jawa.core.container;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jawa.core.util.JawaGlobals;
import org.jawa.core.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * 对web请求起到代理作用的<tt> plugin servlet </tt>
 *
 * <p/>
 * 监听form中的请求 <tt>/plugins/[pluginName]/[JSP File]</tt>
 * (e.g. <tt>/plugins/foo/example.jsp</tt>). 该类同样可以监听form中JSP的请求如：
 * <tt>/plugins/[pluginName]/images/*.png|gif</tt>,
 * <tt>/plugins/[pluginName]/scripts/*.js|css</tt> or
 * <tt>/plugins/[pluginName]/styles/*.css</tt> (e.g.
 * <tt>/plugins/foo/images/example.gif</tt>).<p>
 * <p/>
 * JSP文件必须是编译通过并且经过 插件类加载器加载的可用的文件. 
 * JSP名和 servlet类文件的映射定义在[pluginName]/web/web.xml.
 * 通常情况下，这个文件是在自动打包插件时是由JSP编译器自动生成.
 * 另外如果 <tt>plugin</tt>中设置启用开发模式，那么JSP文件将会被JSPC动态编译
 *
 * @author Matt Tucker
 */
public class PluginServlet extends HttpServlet {

	private static final long serialVersionUID = -4464455988131249934L;

	private static final Logger Log = LoggerFactory.getLogger(PluginServlet.class);

    private static Map<String, GenericServlet> servlets;
    private static PluginManager pluginManager;
    private static ServletConfig servletConfig;

    static {
        servlets = new ConcurrentHashMap<String, GenericServlet>();
    }

    @Override
	public void init(ServletConfig config) throws ServletException {
        super.init(config);
        servletConfig = config;
    }

    @Override
	public void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        String pathInfo = request.getPathInfo();
        if (pathInfo == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
        else {
            try {
                // 处理JSP请求.
                if (pathInfo.endsWith(".jsp")) {
                    if (handleDevJSP(pathInfo, request, response)) {
                        return;
                    }
                    handleJSP(pathInfo, request, response);
                }
                // 处理servlet请求
                else if (getServlet(pathInfo) != null) {
                    handleServlet(pathInfo, request, response);
                }
                // 处理image/其它 请求
                else {
                    handleOtherRequest(pathInfo, response);
                }
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
    }

    /**
     * 注册一个插件的所有JSP页面的servlet。
     *
     * @param manager 插件框架管理实例.
     * @param plugin 插件实例.
     * @param webXML 包含JSP页面名的servlet类文件映射的web.xml文件.
     * 
     */
    public static void registerServlets(PluginManager manager, Plugin plugin, File webXML) {
        pluginManager = manager;
        if (!webXML.exists()) {
            Log.error("注册plugin servlets失败, 文件 " + webXML.getAbsolutePath() + " 不存在.");
            return;
        }
        // 从webXML文件的插件目录中找到的名称
        // plugins/[pluginName]/web/web.xml
        String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName();
        try {
            SAXReader saxReader = new SAXReader(false);
            try {
                saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
                    false);
            }
            catch (SAXException e) {
                Log.warn("设置SAXReader功能错误", e);
            }
            Document doc = saxReader.read(webXML);
            // 找到所有映射类的<servlet>项
            List classes = doc.selectNodes("//servlet");
            Map<String, Class> classMap = new HashMap<String, Class>();
            for (int i = 0; i < classes.size(); i++) {
                Element servletElement = (Element)classes.get(i);
                String name = servletElement.element("servlet-name").getTextTrim();
                String className = servletElement.element("servlet-class").getTextTrim();
                classMap.put(name, manager.loadClass(plugin, className));
            }
            List names = doc.selectNodes("//servlet-mapping");
            for (int i = 0; i < names.size(); i++) {
                Element nameElement = (Element)names.get(i);
                String name = nameElement.element("servlet-name").getTextTrim();
                String url = nameElement.element("url-pattern").getTextTrim();
                // 注册的servlet的URL。
                Class servletClass = classMap.get(name);
                if(servletClass == null) {
                    Log.error("不能加载 servlet, " + name + ", servlet-class 未找到.");
                    continue;
                }
                Object instance = servletClass.newInstance();
                if (instance instanceof GenericServlet) {
                    // 初始化的servlet，然后将其添加到map中
                    ((GenericServlet)instance).init(servletConfig);
                    servlets.put(pluginName + url, (GenericServlet)instance);
                }
                else {
                    Log.warn("不能加载 " + (pluginName + url) + ": 不是一个servlet.");
                }
            }
        }
        catch (Throwable e) {
            Log.error(e.getMessage(), e);
        }
    }

    /**
     * 注销一个插件的所有JSP页面的servlet。
     *
     * @param webXML 包含 JSP 页面名称 servlet 类文件映射到的 web.xml 文件
     */
    public static void unregisterServlets(File webXML) {
        if (!webXML.exists()) {
            Log.error("不能注销插件, 文件 " + webXML.getAbsolutePath() +
                " 不存在.");
            return;
        }
        // 从约定的 webXML文件的插件目录中查找名字
        // plugins/[pluginName]/web/web.xml
        String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName();
        try {
            SAXReader saxReader = new SAXReader(false);
            saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
                false);
            Document doc = saxReader.read(webXML);
            //从查找的所有 <servelt-mapping> 条目
            List names = doc.selectNodes("//servlet-mapping");
            for (int i = 0; i < names.size(); i++) {
                Element nameElement = (Element)names.get(i);
                String url = nameElement.element("url-pattern").getTextTrim();
                //从服务程序映射中删除销毁 servlet 。
                GenericServlet servlet = servlets.get(pluginName + url);
                if (servlet != null) {
                    servlet.destroy();
                }
                servlets.remove(pluginName + url);
                servlet = null;
            }
        }
        catch (Throwable e) {
            Log.error(e.getMessage(), e);
        }
    }

    /**
     * 处理JSP页面请求. 它会检查以查看是否为 JSP URL 映射一个 servlet. </br>
     * 如果找到任何一个，请求处理将会通过. </br>
     * 如果没有找到则返回 404错误. </br>
     *
     * @param pathInfo 额外的路径信息.
     * @param request  request对象实例.
     * @param response response对象实例.
     * @throws ServletException 请求处理时出现的异常.
     * @throws IOException      请求处理时的IO异常.
     */
    private void handleJSP(String pathInfo, HttpServletRequest request,
                           HttpServletResponse response) throws ServletException, IOException {
    	//以"/"开头的字符串
        String jspURL = pathInfo.substring(1);

        GenericServlet servlet = servlets.get(jspURL);
        if (servlet != null) {
            servlet.service(request, response);
        }
        else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    /**
     * Servlet的请求处理器. 如果找到则请求处理pass.</br>
     * 没找到返回404错误.</br>
     *
     * @param pathInfo 额外的路径信息.
     * @param request  request对象实例.
     * @param response response对象实例.
     * @throws ServletException 请求处理时出现的异常.
     * @throws IOException      请求处理时的IO异常.
     */
    private void handleServlet(String pathInfo, HttpServletRequest request,
                               HttpServletResponse response) throws ServletException, IOException {
        // Strip the starting "/" from the path to find the JSP URL.
        GenericServlet servlet = getServlet(pathInfo);
        if (servlet != null) {
            servlet.service(request, response);
        }
        else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    /**
     * 返回与映射匹配的 servlet。
     *
     * @param pathInfo 映射到 servlet的路径信息
     * @return 返回映射的servlet, 如果未找到则返回null.
     */
    private GenericServlet getServlet(String pathInfo) {
        pathInfo = pathInfo.substring(1).toLowerCase();

        GenericServlet servlet = servlets.get(pathInfo);
        if (servlet == null) {
            for (String key : servlets.keySet()) {
                int index = key.indexOf("/*");
                String searchkey = key;
                if (index != -1) {
                    searchkey = key.substring(0, index);
                }
                if (searchkey.startsWith(pathInfo) || pathInfo.startsWith(searchkey)) {
                    servlet = servlets.get(key);
                    break;
                }
            }
        }
        return servlet;
    }


    /**
     * 处理请求的其他 web 项目　如： (images, flash, applets, etc.)
     *
     * @param pathInfo 额外的路径信息.
     * @param response response对象实例.
     * @throws IOException 请求处理时的IO异常.
     */
    private void handleOtherRequest(String pathInfo, HttpServletResponse response) throws IOException {
        String[] parts = pathInfo.split("/");
        // 请求映射必须使用正确的格式.
        if (parts.length < 3) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String contextPath = "";
        int index = pathInfo.indexOf(parts[1]);
        if (index != -1) {
            contextPath = pathInfo.substring(index + parts[1].length());
        }

        File pluginDirectory = new File(JawaGlobals.getHomeDirectory(), "plugins");
        File file = new File(pluginDirectory, parts[1] + File.separator + "web" + contextPath);

        //当使用开发环境时，图像目录可以在其它web网站下
        Plugin plugin = pluginManager.getPlugin(parts[1]);
        PluginDevEnvironment environment = pluginManager.getDevEnvironment(plugin);

        if (environment != null) {
            file = new File(environment.getWebRoot(), contextPath);
        }
        if (!file.exists()) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        else {
        	//文件内容为GIF或PNG.
            String contentType = "image/gif";
            if (pathInfo.endsWith(".png")) {
                contentType = "image/png";
            }
            else if (pathInfo.endsWith(".swf")) {
                contentType = "application/x-shockwave-flash";
            }
            else if (pathInfo.endsWith(".css")) {
                contentType = "text/css";
            }
            else if (pathInfo.endsWith(".js")) {
                contentType = "text/javascript";
            }
            else if (pathInfo.endsWith(".html") || pathInfo.endsWith(".htm")) {
                contentType = "text/html";
            }

            //当下载 CSS时设置content-disposition
            // response.setHeader("Content-disposition", "filename=\"" + file + "\";");
            response.setContentType(contentType);
            InputStream in = null;
            ServletOutputStream out = null;
            try {
                in = new BufferedInputStream(new FileInputStream(file));
                out = response.getOutputStream();

                response.setContentLength((int)file.length());

                // Use a 1K buffer.
                byte[] buf = new byte[1024];
                int len;
                while ((len = in.read(buf)) != -1) {
                    out.write(buf, 0, len);
                }
            }
            finally {
                try {
                    in.close();
                }
                catch (Exception ignored) {
                    // Ignore.
                }
                try {
                    out.close();
                }
                catch (Exception ignored) {
                    // Ignore.
                }
            }
        }
    }


    /**
     * Handles a request for a JSP page in development mode. If development mode is
     * not enabled, this method returns false so that normal JSP handling can be performed.
     * If development mode is enabled, this method tries to locate the JSP, compile
     * it using JSPC, and then return the output.
     *
     * @param pathInfo the extra path info.
     * @param request  the request object.
     * @param response the response object.
     * @return true if this page request was handled; false if the request was not handled.
     */
    private boolean handleDevJSP(String pathInfo, HttpServletRequest request,
                                 HttpServletResponse response) {
		return false;
    	/**
        String jspURL = pathInfo.substring(1);

        // Handle pre-existing pages and fail over to pre-compiled pages.
        int fileSeperator = jspURL.indexOf("/");
        if (fileSeperator != -1) {
            String pluginName = jspURL.substring(0, fileSeperator);
            Plugin plugin = pluginManager.getPlugin(pluginName);

            PluginDevEnvironment environment = pluginManager.getDevEnvironment(plugin);
            // If development mode not turned on for plugin, return false.
            if (environment == null) {
                return false;
            }
            File webDir = environment.getWebRoot();
            if (webDir == null || !webDir.exists()) {
                return false;
            }

            File pluginDirectory = pluginManager.getPluginDirectory(plugin);

            File compilationDir = new File(pluginDirectory, "classes");
            compilationDir.mkdirs();

            String jsp = jspURL.substring(fileSeperator + 1);

            int indexOfLastSlash = jsp.lastIndexOf("/");
            String relativeDir = "";
            if (indexOfLastSlash != -1) {
                relativeDir = jsp.substring(0, indexOfLastSlash);
                relativeDir = relativeDir.replaceAll("//", ".") + '.';
            }

            File jspFile = new File(webDir, jsp);
            String filename = jspFile.getName();
            int indexOfPeriod = filename.indexOf(".");
            if (indexOfPeriod != -1) {
                filename = "dev" + StringUtils.randomString(4);
            }

            JspC jspc = new JspC();
            if (!jspFile.exists()) {
                return false;
            }
            try {
                jspc.setJspFiles(jspFile.getCanonicalPath());
            }
            catch (IOException e) {
                Log.error(e.getMessage(), e);
            }
            jspc.setOutputDir(compilationDir.getAbsolutePath());
            jspc.setClassName(filename);
            jspc.setCompile(true);

            jspc.setClassPath(getClasspathForPlugin(plugin));
            try {
                jspc.execute();

                try {
                    Object servletInstance = pluginManager.loadClass(plugin, "org.apache.jsp." +
                        relativeDir + filename).newInstance();
                    HttpServlet servlet = (HttpServlet)servletInstance;
                    servlet.init(servletConfig);
                    servlet.service(request, response);
                    return true;
                }
                catch (Exception e) {
                    Log.error(e.getMessage(), e);
                }

            }
            catch (JasperException e) {
                Log.error(e.getMessage(), e);
            }
        }
        return false;
        **/
    }

    /**
     * 使用 JSPC 编译器返回classpath.
     *
     * @param plugin 将处理的jspc 插件.
     * @return 在插件中的单个 jsp 编译所需的类路径。
     */
    private static String getClasspathForPlugin(Plugin plugin) {
        final StringBuilder classpath = new StringBuilder();

        File pluginDirectory = pluginManager.getPluginDirectory(plugin);

        PluginDevEnvironment pluginEnv = pluginManager.getDevEnvironment(plugin);

        PluginClassLoader pluginClassloader = pluginManager.getPluginClassloader(plugin);

        for (URL url : pluginClassloader.getURLs()) {
            File file = new File(url.getFile());

            classpath.append(file.getAbsolutePath()).append(";");
        }

        // 加载所有LIB目录的JAR包
        File libDirectory = new File(pluginDirectory, "lib");
        File[] libs = libDirectory.listFiles();
        final int no = libs != null ? libs.length : 0;
        for (int i = 0; i < no; i++) {
            File libFile = libs[i];
            classpath.append(libFile.getAbsolutePath()).append(';');
        }

        File openfireRoot = pluginDirectory.getParentFile().getParentFile().getParentFile();
        File openfireLib = new File(openfireRoot, "target//lib");

        classpath.append(openfireLib.getAbsolutePath()).append("//servlet.jar;");
        classpath.append(openfireLib.getAbsolutePath()).append("//openfire.jar;");
        classpath.append(openfireLib.getAbsolutePath()).append("//jasper-compiler.jar;");
        classpath.append(openfireLib.getAbsolutePath()).append("//jasper-runtime.jar;");

        if (pluginEnv.getClassesDir() != null) {
            classpath.append(pluginEnv.getClassesDir().getAbsolutePath()).append(";");
        }
        return classpath.toString();
    }
}
